上一篇30天Flutter手滑系列 - 無狀態與有狀態Widgets (Stateless & Stateful widgets),算是基礎的狀態管理教學,今天開始才是正式進入進階的部分。
在開發初期,只需將狀態直接反映在View上即可:
但隨著應用程式的功能日益增多,很可能變成這樣子:
一團混亂了!
這個問題不管在Web或是Mobile的開發,往往是個需要被克服的問題,像是React就衍伸出利用Redux這個第三方套件來有效管理組件間的資料變化,在Flutter中也借鏡了React一些觀念。在這個章節,會介紹Flutter內建的狀態管理方法,以及部分熱門狀態管理套件。
如一章節提到的,內建的setState
是其中一種狀態管理辦法。在flutter create app
的預設程式碼中,就展示了setState的用法。
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
如果需要跨組件傳遞,需要把傳遞的任務放到parent的widget上。由於每次setState需要rebuild widgets,如果是隔很多層的傳遞,需要被rebuild的widgets會越多,相對效能會比較差,僅適合小規模的更新。
單純setState實在太過笨重沒效率,因此官方提出了InheritedWidget
這個Widget。
其核心理念是在父層級的Widget加入一層InheritedWidget,讓在其底下的Widgets可以參考到這個InheritedWidget底下的節點。
來看一下以下的官方範例:
(1) 首先建立一個InheritedWidget
(2) 建立Data資料
(3) 加入of方法
(4) 加入updateShouldNotify方法:告訴flutter有資料更新時需要重新繪製
// (1)建立一個subclass InheritedWiget
class FrogColor extends InheritedWidget {
// (2)建立Data
const FrogColor({
Key key,
@required this.color,
@required Widget child,
}) : assert(color != null),
assert(child != null),
super(key: key, child: child);
final Color color;
// (3)加入of方法
static FrogColor of(BuildContext context) {
return context.inheritFromWidgetOfExactType(FrogColor) as FrogColor;
}
@override
// (4)加入updateShouldNotify方法
// (5)並控制當接收到更新
bool updateShouldNotify(FrogColor old) => color != old.color;
}
InheritedWidget改善了撰寫的麻煩,不過同樣有些缺點:
- 無法將View與Logic部分分開。
- 無法定向通知。
- 每次更新都會通知所有Widgets,無法區分哪些Widgets需要被更新。解決辦法可以透過
StreamBuilder
來監聽InheritedWidget中的stream
的資料變化,然後判斷是否更新當前widget。
Scoped Model主要透過Model的概念實作資料的傳遞,類似於React中的context
概念。本身提供的方法,可以讓子Widget存取父Widget的model功能。
簡單的三步驟可以創建一個scoped_model
:
Model
Class,然後自定義你的Model,像是CountModel或SearchModel,並在狀態改變時執行notifyListeners()
。ScopedModel
去包裝你的Model,這樣可以使這Model被所有Widgets使用。ScopedModelDescendant
去找到Widget tree中目標的ScopedModel
,使它可以根據狀態更新自動去rebuild widget。class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
// 狀態改變通知所有listeners
notifyListeners();
}
}
// 創建一個CounterModel
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 首先創建一個ScopedModel讓model可以被所有子widgets存取
return new ScopedModel<CounterModel>(
model: new CounterModel(),
child: new Column(children: [
// 加入ScopedModelDescendant,
// 它會從最近的ScopedModel<CounterModel>找到CounterModel。
// 然後接收到CounterModel改變時去進行rebuild
new ScopedModelDescendant<CounterModel>(
builder: (context, child, model) => new Text('${model.counter}'),
),
new Text("Another widget that doesn't depend on the CounterModel")
])
);
}
}
ScopedModel
的優缺點跟InheritedWidget基本上一樣,因為他只是封裝自InheritedWidget而已。
Bloc
是Business Logic Component的縮寫,是一種設計模式。其核心思想是讓UI與數據分離,以數據驅動去渲染UI,概念跟Redux
一樣。
Bloc
同時是一種以Reactive Programming
去開發的方法,一切都是stream
的概念。只需用StreamBuilder
和Bloc
,就可以讓業務邏輯被獨立出來,不用考慮何時需要去更新View的部分。
透過stream可以傳遞事件、值、對象或集合等。
當需要知道stream的某些內容時,只需要訂閱StreamController的stream對象,透過被訂閱的StreamSubscription對象,就可以知道stream發生變化進而觸發通知。
stream可以想像成是一個水管,水通過水管,一路向下流。如果有學過RxJS的人應該不陌生。
在實作上可以直接使用flutter_bloc,而不需要自行定義stream的操作。
stream有兩種類型:
import 'dart:async';
void main() {
// 初始化一個Single-Subscription StreamController
final StreamController ctrl = StreamController();
// 定義一個監聽器,目的是在收到資料時候直接印出來
final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));
/ 新增流入stream的資料
ctrl.sink.add('my name');
ctrl.sink.add(1234);
ctrl.sink.add({'a': 'element A', 'b': 'element B'});
ctrl.sink.add(123.45);
// 結束釋放StreamController
ctrl.close();
}
import 'dart:async';
void main() {
// 初始化一個int型態的Broadcast StreamController
final StreamController<int> ctrl = StreamController<int>.broadcast();
// 定義一個監聽器,目的是過濾掉奇數,只印出偶數。
final StreamSubscription subscription = ctrl.stream
.where((value) => (value % 2 == 0))
.listen((value) => print('$value'));
// 新增流入stream的資料
for(int i=1; i<11; i++){
ctrl.sink.add(i);
}
// 結束釋放StreamController
ctrl.close();
}
哪一個套件好用,我覺得見仁見智,挑適合專案或團隊的就好,或者說能解決問題的都是好方法。
https://flutter.dev/docs/development/data-and-backend/state-mgmt
https://hicc.me/flutter-state-management/
https://pub.flutter-io.cn/packages/scoped_model#-readme-tab-
https://juejin.im/post/5cd91bb0f265da034e7eaca3
https://juejin.im/post/5bb6f344f265da0aa664d68a
https://juejin.im/post/5b97fa0d5188255c5546dcf8
https://juejin.im/post/5c3ef2695188252547423234
https://wizardforcel.gitbooks.io/gsyflutterbook/content/Flutter-12.html
https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/